﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.IO;
using System.Reflection;
using System.Windows.Browser;
using System.Web;
using SilverQuery;

namespace CodeGen
{
	class Program
	{
		// Methods that may require special handling:  show(
		private static List<string> ignoredCategories = new List<string>()
		{
			"Internals", "Utilities", "Ajax"
		};

		private static List<string> ignoredFunctions = new List<string>()
		{
			"jQuery", "each", "jQuery.fn.extend", "jQuery.extend", 
			"jQuery.noConflict", "queue", "dequeue", "ready", "map", "offset", "get", "bind", "unbind"		
		};

		static void Main(string[] args)
		{
			XmlDocument xml = new XmlDocument();
			xml.Load("jQueryDocs.xml");

			IList<MethodInfo> methods = LoadMethods(xml.SelectNodes("//function"));

			using (var sw = new StreamWriter("jQuery.CodeGen.cs"))
			{
				WriteClassHeader(sw);
								
				foreach (MethodInfo method in methods)
				{
					WriteXmlComment(sw, method);
					WriteMethodSignature(sw, method);
					sw.WriteLine("\t\t{");

					WriteMethodBody(sw, method);

					sw.WriteLine("\t\t}");
				}

				sw.WriteLine("\t}");
				sw.WriteLine("}");
				sw.Flush();
			}
		}

		private static IList<MethodInfo> LoadMethods(XmlNodeList functions)
		{
			var list = new List<MethodInfo>();
			foreach (XmlElement function in functions)
			{
				if (ignoredCategories.Contains(function.GetAttribute("cat")))
					continue;
				if (ignoredFunctions.Contains(function.GetAttribute("name").Trim()))
					continue;

				MethodInfo method = LoadMethod(function);
				if (list.Contains(method))
					continue;

				var methods = new List<MethodInfo>();
				methods.Add(method);
				
				// If the methods last parameter is optional, then create an overloaded
				// version of the method without that parameter.
				for (int i = method.Parameters.Count - 1; i >= 0; i--)
				{
					if (!method.Parameters[i].Optional)
						break;

					MethodInfo clone = method.Clone();
					clone.Parameters = new List<ParameterInfo>(method.Parameters.Take(i));
					methods.Add(clone);
				}

				foreach (MethodInfo m in methods.ToArray())
				{
					if (m.Parameters.Count == 0)
						continue;

					// Special handling for methods whose first parameter is named "speed".  
					// In these cases create an overloaded version of the method
					// where the speed parameter is an int.
					if (m.Parameters[0].Name == "speed")
						AppendEffectOverloads(methods, m);

					if (m.Parameters.Count == 1 && m.Parameters[0].Name == "content" && m.Parameters[0].Type == typeof(String))
					{
						MethodInfo clone = m.Clone();
						clone.Parameters[0].Type = typeof(jQuery);
						methods.Add(clone);
					}
				}

				foreach (MethodInfo m in methods)
				{
					if (!list.Contains(m))
						list.Add(m);
				}
			}
			return list;
		}

		private static void AppendEffectOverloads(List<MethodInfo> methods, MethodInfo m)
		{
			m.Parameters[0].Description = "A string representing one of the three predefined speeds (\"slow\", \"normal\", or \"fast\")";
			MethodInfo clone = m.Clone();
			clone.Parameters[0].Type = typeof(int);
			clone.Parameters[0].Description = "The number of milliseconds to run the animation (e.g. 1000)";
			methods.Add(clone);

			// Create a second clone of the method that takes no parameters, i.e. fadeIn();
			if (m.Parameters.Count == 1)
			{
				MethodInfo clone2 = m.Clone();
				clone2.Parameters.Clear();
				methods.Add(clone2);
			}
		}

		private static void WriteXmlComment(StreamWriter sw, MethodInfo method)
		{
			var summary = method.Summary.Replace("\r\n", string.Empty);

			sw.WriteLine("\t\t/// <summary>");
			foreach (string line in summary.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries))
			{
				sw.Write("\t\t/// ");
				sw.WriteLine(HttpUtility.HtmlEncode(line));
			}
			sw.WriteLine("\t\t/// </summary>");
			foreach (ParameterInfo p in method.Parameters)
			{
				sw.Write("\t\t/// <param name=\"");
				sw.Write(p.Name);
				sw.Write("\">");
				if (!String.IsNullOrEmpty(p.Description))
					sw.Write(p.Description);
				sw.WriteLine("</param>");
			}
			sw.Write("\t\t/// <returns>");
			sw.Write(GetTypeOutputString(method.ReturnType));
			sw.WriteLine("</returns>");
		}

		private static MethodInfo LoadMethod(XmlElement element)
		{
			var method = new MethodInfo()
			{
				Name = element.GetAttribute("name").Trim(),
				ReturnType = GetManagedType(element.GetAttribute("return").Trim()),
				Category = element.GetAttribute("cat"),
				Summary = element.SelectSingleNode("desc").InnerText.Trim()
			};

			method.Parameters = new List<ParameterInfo>();
			var list = new List<ParameterInfo>();
			foreach (XmlElement e in element.GetElementsByTagName("params"))
			{
				var parameter = new ParameterInfo()
				{
					Name = FixParameterName(e.GetAttribute("name").Trim()),
					Optional = e.GetAttribute("optional") == "true"
				};
			
				// Check if there is already a parameter with this name, if so append a 1.
				if (method.Parameters.Any(p => p.Name == parameter.Name))
					parameter.Name += "1";
				
				string parameterType = e.GetAttribute("type").Trim();
				if (parameterType == "Function")
				{
					if (method.Category == "Events")
						parameter.Type = typeof(EventCallback);
					else
						parameter.Type = typeof(Callback);
				}
				else if (parameter.Name == "opacity")
					parameter.Type = typeof(Double);
				else
					parameter.Type = GetManagedType(parameterType);
				
				XmlElement desc = (XmlElement)e.SelectSingleNode("desc");
				if (desc != null)
				{
					int i = desc.InnerText.IndexOfAny(new[] { '\r', '\n' });
					parameter.Description = (i != -1) ? desc.InnerText.Substring(0, i) : desc.InnerText;
				}
				method.Parameters.Add(parameter);
			}
			return method;
		}
			
		private static void WriteMethodSignature(TextWriter writer, MethodInfo method)
		{
			writer.Write("\t\tpublic {0} {1}(", method.ReturnType.Name, FixMethodName(method.Name));
			for (int i = 0; i < method.Parameters.Count; i++)
			{
				if (i > 0)
					writer.Write(", ");

				writer.Write("{0} {1}", GetTypeOutputString(method.Parameters[i].Type), method.Parameters[i].Name);
			}

			writer.WriteLine(")");
		}

		private static void WriteMethodBody(TextWriter writer, MethodInfo method)
		{
			writer.Write("\t\t\treturn ");
			if (method.ReturnType == typeof(jQuery))
			{
				string invokeMethod;
				if (method.Category == "Traversing")
					invokeMethod = "InvokeAndReturnNewQuery";
				else
					invokeMethod = "InvokeAndReturnSelf";

				WriteInvokeMethodCall(writer, method, invokeMethod);
			}
			else if (method.ReturnType == typeof(int))
			{
				WriteInvokeMethodCall(writer, method, "InvokeAndReturnInt");
			}
			else
			{
				WriteInvokeMethodCall(writer, method, "InvokeAndReturn<" + method.ReturnType.Name + ">");
			}
		}

		private static void WriteInvokeMethodCall(TextWriter writer, MethodInfo method, string invokeMethod)
		{
			// Write the method body.
			writer.Write(invokeMethod);
			writer.Write("(\"");
			writer.Write(method.Name);
			writer.Write("\"");
			for (int i = 0; i < method.Parameters.Count; i++)
			{
				Type paramType = method.Parameters[i].Type;
				writer.Write(", ");

				if (paramType == typeof(EventCallback))
					writer.Write("SilverQueryBridge.GetScriptEventCallback(" + method.Parameters[i].Name + ")");
				else if (paramType == typeof(Callback))
					writer.Write("SilverQueryBridge.GetScriptCallback(" + method.Parameters[i].Name + ")");
				else if (paramType == typeof(map))
					writer.Write(method.Parameters[i].Name + ".ToScriptObject()");
				else if (paramType == typeof(jQuery))
					writer.Write(method.Parameters[i].Name + ".NativeQuery");
				else
					writer.Write(method.Parameters[i].Name);
			} 
			writer.WriteLine(");");
		}

		private static Type GetManagedType(string scriptType)
		{
			// Handle type attributes like "String ''(optional)''" and "String, Array".
			int index = scriptType.IndexOfAny(new char[] { ' ', ',' });
			if (index != -1)
				scriptType = scriptType.Substring(0, index);

			switch (scriptType)
			{
				case "jQuery": return typeof(jQuery);
				case "Boolean": return typeof(bool);
				case "Selector":
				case "Object":
				case "Expression":
				case "string":
				case "String": return typeof(string);
				case "Integer":
				case "Number": return typeof(int);
				case "Options":
				case "Map": return typeof(map);
				case "Array":
				case "Array<String>": return typeof(string[]);
				case "Element": return typeof(HtmlElement);
				case "Array<Element>": return typeof(HtmlElement[]);

				default : throw new InvalidOperationException("Could not map scriptType '" + scriptType + "' to an equivalent managed type.");
			}
		}

		private static string FixParameterName(string paramName)
		{
			switch (paramName)
			{
				case "class": return "className";
				case "params": return "parameters";
				case "out": return "off";
				case "true": return "deep";
				default: return paramName;
			}
		}

		private static string FixMethodName(string methodName)
		{
			switch (methodName)
			{
				// The term "is" is a reserved keyword in C#.
				case "is": return "isMatch";
				default: return methodName;
			}
		}

		private static string GetTypeOutputString(Type type)
		{
			//if (type == typeof(IDictionary<string, string>))
			//  return "IDictionary<string, string>";
			//else
				return type.Name;
		}

		private static void WriteStartRegion(StreamWriter sw, XmlElement element)
		{
			sw.WriteLine("\t\t#region {0}", element.Attributes["value"].Value);
		}

		private static void WriteEndRegion(StreamWriter sw)
		{
			sw.WriteLine("\t\t#endregion");
		}

		private static void WriteClassHeader(StreamWriter sw)
		{
	
			sw.WriteLine("using System;");
			sw.WriteLine("using System.Text;");
			sw.WriteLine("using System.Collections;");
			sw.WriteLine("using System.Collections.Generic;");
			sw.WriteLine("using System.Windows;");
			sw.WriteLine("using System.Windows.Browser;");
			
			sw.WriteLine();

			sw.WriteLine("namespace SilverQuery {");
			sw.WriteLine("\t/// <summary>");
			sw.WriteLine("\t/// This partial class is autogenerated by the SilverQuery CodeGen application by reading");
			sw.WriteLine("\t/// from the jQuery xml documentation file.  Do not modify this file.");
			sw.WriteLine("\t/// </summary>");
			sw.WriteLine("\tpublic partial class jQuery {");
		}

		private class MethodInfo
		{
			public string Name { get; set; }
			public Type ReturnType { get; set; }
			public string Summary { get; set; }
			public IList<ParameterInfo> Parameters { get; set; }
			public string Category { get; set; }

			public override bool Equals(object obj)
			{
				var m = (MethodInfo)obj;

				if (m.Name != this.Name)
					return false;

				if (m.Parameters.Count != this.Parameters.Count)
					return false;

				for (int i = 0; i < m.Parameters.Count; i++)
				{
					if (m.Parameters[i].Type != this.Parameters[i].Type)
						return false;
				}
				return true;
			}

			internal MethodInfo Clone()
			{
				var m = new MethodInfo();
				m.Name = this.Name;
				m.ReturnType = this.ReturnType;
				m.Summary = this.Summary;
				m.Category = this.Category;

				m.Parameters = new List<ParameterInfo>(this.Parameters.Count);
				foreach (var p in this.Parameters)
				{
					m.Parameters.Add(p.Clone());
				}
				return m;
			}
		}

		private class ParameterInfo
		{
			public Type Type { get; set; }
			public string Name { get; set; }
			public string Description { get; set; }
			public bool Optional { get; set; }

			internal ParameterInfo Clone()
			{
				var p = new ParameterInfo();
				p.Name = this.Name;
				p.Description = this.Description;
				p.Type = this.Type;
				p.Optional = this.Optional;
				return p;
			}
		}
	}
}
